UnityOvercooked

Author::Magialeaf/FakeOvercooked: Overcooked仿制版 (github.com)

一、环境配置

1 新建项目

新建项目新建的是3D(URP)项目。

Windows——Package Manager——可以移除Visual Studio Code Editor,因为版本老。其他的也可以不用管。

Project自带:

  • Scenes——SmapleScene:简单案例。
  • TutorialInfo——Readme:URP相关教程,可以移除。
  • Settings:
    • SampleSceneProfile:默认的SampleScene会有一个Global Volume,其中Volume的Profile需要设置为SampleSceneProfile,也没什么用,可以删除。
    • URP*:渲染配置文件,共六个,分别是Performant(性能优先)、Balanced(平衡)、HighFidelity(高精度)
  • UniveralRenderPipelineGlobalSettings:URP全局设置。也可以在Edit——Project Settings——Graphics——URP Global Settings中设置。

设置渲染等级:

  • Edit——Project Settings——Graphics:选择渲染脚本。
  • Edit——Project Settings——Quality:选择渲染质量。

新建URP相关:右键——create——Rendering

2 VSCode开发

如果使用VStudio开发忽略。

配置:

  • VSCode中配置:File——Preferences——Settings——输入useModernNet,设置为False。
  • Unity中配置:Edit——Preferences——External Tools——External Script Editor选择VSCode(没有就Browse到VSCode)。然后设置External Script Editor Args为:"$(ProjectPath)" -g "$(File)":$(Line):$(Column)(否则在VSCode中代码引用会找不到其他文件的引用结果,只能找到同一文件下的引用结果)

安装插件:

EditorConfig for VS Code使用:

  1. 安装插件
  2. 在项目根目录下输入新建文件夹:.editorconfig
  3. 输入下列代码
  4. File——Preferences——Settings——输入editor.formatOnSave,确保Editor: Format On Save勾选,这样每次保存时就会格式化代码。
// .editorconfig(使用时把这一段注释删除)

root = true

[*]
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
file_header_template = "123"

3 VS开发

编码问题:保存的文件是当前系统的语言(可能是GB2312),而非UTF-8。解决方式:扩展——管理扩展——搜素Force UTF-8 (With BOM)并下载即可。

或者用python脚本

# 限定保存编码默认是 BG2312
import os
import codecs
def detect_and_convert(file_path, target_encoding='utf-8-sig'):
detected_encoding = None
try:
# 尝试以GB2312读取
with codecs.open(file_path, 'r', encoding='gb2312') as f:
f.read() # 仅尝试读取,成功则说明是GB2312
detected_encoding = 'gb2312'
except UnicodeDecodeError:
# 如果GB2312失败,再尝试UTF-8-SIG
try:
with codecs.open(file_path, 'r', encoding='utf-8-sig') as f:
f.read()
detected_encoding = 'utf-8-sig'
except UnicodeDecodeError:
print(f"Cannot determine encoding for '{file_path}', skipping.")
return

# 如果检测到了编码且与目标编码不同,则进行转换
if detected_encoding and detected_encoding != target_encoding:
try:
with codecs.open(file_path, 'r', encoding=detected_encoding) as f:
content = f.read()
with codecs.open(file_path, 'w', encoding=target_encoding) as f:
f.write(content)
print(f"Converted '{file_path}' from {detected_encoding} to {target_encoding}")
except Exception as e:
print(f"Error converting '{file_path}': {str(e)}")

def convert_cs_files_to_utf8_with_bom(root_folder):
for folder_name, _, file_names in os.walk(root_folder):
for file_name in file_names:
if file_name.endswith('.cs'):
file_path = os.path.join(folder_name, file_name)
try:
detect_and_convert(file_path)
except Exception as e:
print(f"Error detecting encoding of '{file_path}': {str(e)}")

if __name__ == "__main__":
folder_path = '.' # Change this to the directory you want to start the conversion from
convert_cs_files_to_utf8_with_bom(folder_path)


二、前期处理

1 后处理效果

在Hierarchy找到Global Volume——在Inspector中的Volume找到Profile——New。然后在Project中可以看到出现了一个和scene名一样的文件下,文件夹下有一个文件Global Volume Profile。

然后在Global Volume的Inspector中点击Add Override可以添加后处理的效果。

后处理效果如下:

  • Tonemapping:色调映射,给场景做一个HDR处理。勾选Mode后选择属性可以加上Mode:None(无)、Neutral(自然)、ACES(一种标准)
  • Color Adjustments:色彩调整,给色彩做一个基本调整。
    • Post Exposure(曝光度):值越大越亮。
    • Contrast(色差):值越大,亮的地方越亮,暗的地方越暗。
    • Color Filter(色温):整体颜色。
    • Hue Shift(色调):图像的相对明暗程度。
    • Saturation(饱和度):值越大,颜色越鲜艳。
  • Bloom:泛光效果。
    • Threshold(阈值):某个物体的亮度超过这个阈值才会出现泛光效果。
    • Intensity(强度):泛光的强度。
  • Vignette:相机四角阴影效果。(暗角处理,有利于视线集中在中心)
    • Intensity(强度):越高阴影越大。
    • smoothness(平滑度):越高阴影边界越柔和。

2 抗锯齿处理

Hierarchy——Camera——Rendering——Anti-allasing:FXAA(粗糙的模糊处理) / SMAA(性耗比最佳的处理)。(内置渲染管线)

Project——Settings——URP-HighFidelity——Quality——Anti-allasing(MSAA)。(URP渲染)

如果使用了URP,且同时使用了Camera和URP抗锯齿,URP的会覆盖Camera的。

Project——Settings——URP-HighFidelity-Renderer——Screen Space Ambient Occlusion:修改软阴影(物体拐角地方的阴影)

三、脚本

1 函数技巧

// 假设从OA到OB,Lerp是直线AB,所以会中间快两边慢。适合两个坐标移动。
transform.forward = Vector3.Lerp(transform.forward, director, Time.deltaTime * moveSpeed);

// 假设从OA到OB,SLerp是弧线AB,每个地方都保持一致。适合两个向量移动。
transform.forward = Vector3.SLerp(transform.forward, director, Time.deltaTime * moveSpeed);

2 输入系统

使用新版输入系统:在Edit——Project Settings——Player——Active Input Handing中选择Input System Package(New)或者Both。

设置了新版输入系统后要导入包:Window——Package Manager——Packages:Untiy Registry——Input System。

然后在Project中新建Input Actions,在里面设置映射。

在Input Actions的Inspector中,可以点击Generate C# class生成类直接使用

重新绑定注意:

  1. 重新绑定的数据不会直接影响默认的GameControl,因为程序是new GameControl(),影响的是新建出来的对象,所以重新启动后按键绑定会恢复。
  2. 重新绑定时,需要先gameControl.Player.Disable()禁用对象,再进行绑定,后续再启动对象。
// 输入控制
public class GameInput : MonoBehaviour
{
private GameControl gameControl;

public void Awake()
{
gameControl = new GameControl();
// 启用某个map操作方式,gameControl.[mapName].Enable()
gameControl.Player.Enable();
// 禁用某个map操作方式
// gameControl.Player.Disable();
}

public Vector3 GetMovementDirectionNormalized()
{
// 获得某个map的actions值gameControl.[mapName].[actionsName].Enable()
Vector2 inputVector2 = gameControl.Player.Move.ReadValue<Vector2>();
Vector3 direction = new Vector3(inputVector2.x, 0, inputVector2.y);

direction = direction.normalized;

return direction;
}
}
// 重新绑定案例
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
print("开始绑定");
gameControl.Player.Disable();
gameControl.Player.Move.PerformInteractiveRebinding(1).OnComplete(callback =>
{
// 新绑定的按键
print(callback.action.bindings[1].path);
// 原来的按键
print(callback.action.bindings[1].overridePath);
// 绑定
callback.Dispose();
print("绑定完成");
// 重新启用
gameControl.Player.Enable();
}).Start();
}
}
// 详细案例
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;

public class GameInput : MonoBehaviour
{
public static GameInput Instance { get; private set; }

private const string GAME_INPUT_BINDINGS = "GameInputBindings";

public event EventHandler OnInteractAction;
public event EventHandler OnOperationAction;
public event EventHandler OnPauseAction;

private GameControl gameControl;

public enum BindingType
{
Up,
Down,
Left,
Right,
Interact,
Operation,
Pause
}

public void Awake()
{
Instance = this;
gameControl = new GameControl();
if (PlayerPrefs.HasKey(GAME_INPUT_BINDINGS))
{
// 重新绑定信息
gameControl.LoadBindingOverridesFromJson(PlayerPrefs.GetString(GAME_INPUT_BINDINGS));
}
gameControl.Player.Enable();

gameControl.Player.Interact.performed += Interact_Performed;
gameControl.Player.Operation.performed += Operation_Performed;
gameControl.Player.Pause.performed += Pause_Performed;
}

private void OnDestroy()
{
gameControl.Player.Interact.performed -= Interact_Performed;
gameControl.Player.Operation.performed -= Operation_Performed;
gameControl.Player.Pause.performed -= Pause_Performed;

gameControl.Dispose();
}

private void Interact_Performed(UnityEngine.InputSystem.InputAction.CallbackContext obj) => OnInteractAction?.Invoke(this, EventArgs.Empty);
private void Operation_Performed(UnityEngine.InputSystem.InputAction.CallbackContext obj) => OnOperationAction?.Invoke(this, EventArgs.Empty);
private void Pause_Performed(UnityEngine.InputSystem.InputAction.CallbackContext obj) => OnPauseAction?.Invoke(this, EventArgs.Empty);


public Vector3 GetMovementDirectionNormalized()
{
// 获得对应actions值gameControl.[mapName].[actionsName].Enable()
Vector2 inputVector2 = gameControl.Player.Move.ReadValue<Vector2>();
Vector3 direction = new Vector3(inputVector2.x, 0, inputVector2.y);

direction = direction.normalized;

return direction;
}

public void ReBinding(BindingType bindingType, Action onComplete)
{
InputAction inputAction = null;
int index = -1;
switch (bindingType)
{
case BindingType.Up:
index = 1;
inputAction = gameControl.Player.Move;
break;
case BindingType.Down:
index = 2;
inputAction = gameControl.Player.Move;
break;
case BindingType.Left:
index = 3;
inputAction = gameControl.Player.Move;
break;
case BindingType.Right:
index = 4;
inputAction = gameControl.Player.Move;
break;
case BindingType.Interact:
index = 0;
inputAction = gameControl.Player.Interact;
break;
case BindingType.Operation:
index = 0;
inputAction = gameControl.Player.Operation;
break;
case BindingType.Pause:
index = 0;
inputAction = gameControl.Player.Pause;
break;
default:
break;
}

gameControl.Player.Disable();
inputAction.PerformInteractiveRebinding(index).OnComplete(callback =>
{
callback.Dispose();
gameControl.Player.Enable();
onComplete?.Invoke();

// gameControl.SaveBindingOverridesAsJson(); 获得绑定的JSON数据(不会保存)
PlayerPrefs.SetString(GAME_INPUT_BINDINGS, gameControl.SaveBindingOverridesAsJson());
// 手动保存(不手动也一样会自动保存,但是是当数据量达到一定值以后才自动保存)
PlayerPrefs.Save();
}).Start();

}



public string GetBindingDisplayString(BindingType bindingType)
{
switch (bindingType)
{
case BindingType.Up:
return gameControl.Player.Move.bindings[1].ToDisplayString();
case BindingType.Down:
return gameControl.Player.Move.bindings[2].ToDisplayString();
case BindingType.Left:
return gameControl.Player.Move.bindings[3].ToDisplayString();
case BindingType.Right:
return gameControl.Player.Move.bindings[4].ToDisplayString();
case BindingType.Interact:
return gameControl.Player.Interact.bindings[0].ToDisplayString();
case BindingType.Operation:
return gameControl.Player.Operation.bindings[0].ToDisplayString();
case BindingType.Pause:
return gameControl.Player.Pause.bindings[0].ToDisplayString();
default:
return string.Empty;
}
}
}

3 数据存储

数据对象直接继承ScriptableObject,这样的话这个脚本可以直接在Unity中创建这个对象,同时可以在本地进行持久化保存。

文件夹命名文件夹是ScriptObjects,脚本是...SO

// 加上[CreateAssetMenu]后可以直接在project位置用右键新建出这个对象来。
[CreateAssetMenu]
public class KitchenObjectSO : ScriptableObject
{
public GameObject prefab;
public Sprite sprite;
public string objectName;
}

四、UI

1 字体制作

中文字体制作:Window——TextMeshPro——Font Asset Creator。Source Font File选择中文字体ttf文件(文件名要纯英文),Character Set 选择 Characters from File(文件名要纯英文),然后Character FIle选择一个Text文件,Text中写上需要用的字符(一般是需要用的中文+英文大小写+数字+下划线),然后点击Generate Font Atlas,生成后点Save。

其他选项:

  • Atlas Resolution:字体大小,一般在保证清晰的情况下越小越好。

使用:在TextMeshPro对象中,修改Font Asset为自己制作的Font Asset即可。

后续修改:找到字体文件(TMP_Font Asset),点击Update Atlas Texture,重新制作后点击Save即可。


五、Shader

1 Shader Graph

新建:Assets中——Create——Shader Graph——URP——Lit Shader Graph

打开:双击打开。

简单Shader步骤:

  1. 在界面中右键,新建一个Node(直接搜索Sample Texture 2D)。
  2. 在左边的MovingVisual中新建一个Texture 2D,并拖动到面板中。
  3. 输入:连接两者,连接的地方是Node的Texture(T2)的位置。
  4. 输出:点击Node的RGBA(4),这是输出的贴图,连接到Fragment的Base Color(3)上。
  5. 点击左边的MovingVisual中的对象,在右边的Node Settings中,为它设置一张Default贴图。

其他操作:

  • Main Preview中会显示材质结果,右键可以切换显示的对象,比如切换成Cube等。
  • 左上角Save Asset进行保存。

使用Shader Graph:

  1. 在Asset中创建一个Material,然后选择Shader。
  2. 新建物体,使用这个Material。

六、网络

1 Netcode for GameObjects

安装:Windows——Package Manager——Netcode for GameObjects。

注意版本(2021.3对应包版本1.2,安装最新的可能报错):Windows——Package Manager——左上角"+"——Add package by name…——name输入的是com.unity.netcode.gameobjects,version输入1.2.0

使用:新建对象NetworkManager(不能作为子物体),然后添加脚本NetworkManager。

配置脚本Network Manager:

  • Player Prefab:添加网络中传递的预制件(可以是Player,然后给Player加上Network Object脚本)。
  • Select transport:选择Unity Transport。

配置Unity Transport:

  • Connection Data:连接的ip和port

七、问题

1 碰撞

Q1:小人碰到物体后会倒下。

A1:Position锁定Z轴,因为是平面游戏。Rotation锁定X、Z轴,因为小人只需要饶Y轴旋转。

Q2:小人移动卡墙闪烁。

A1:因为刚体碰撞和Update()检测频率不一致导致的。把移动放在FixedUpdate()即可(一般物理移动代码是放在FixedUpdate(),其他的是Update())。

2 Canvas

Q1:Canvas有一个白色边框。

A1:在Game面板右上角——Gizmos——取消掉Canvas(找不到就搜索)。

3 粒子

Q1:粒子直接挂载在人物下不显示。

A1:带有刚体的物体会影响粒子的播放。可以选择将粒子直接放到根目录下,然后挂上FollowTarget的脚本。

// FollowTarget
using UnityEngine;

public class FollowTarget : MonoBehaviour
{
[SerializeField] private Transform target;
private void FixedUpdate()
{
transform.position = target.position;
}
}

4 触发器

Q1:做塔防时,塔的触发器遮挡住了鼠标的射线检测,无法放下防御塔。

A1:Edit——Project Settings——Physics——取消Queries Hit Triggers,这样就不会检查触发器了,只会检查碰撞器。